package heap;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

import chainexception.ChainException;
import global.GlobalConst;
import global.PageId;
import global.Minibase;
import global.Page;


public class Directory extends Page {
	private int size;
	private int count;

	/* Constructor */
	public Directory(Page page) throws ChainException{
		data = page.getData();
		size = getSize(new Entry());
		count = GlobalConst.PAGE_SIZE / size;
	}


	public void formatDirectory() throws ChainException{
		Entry initHF = new Entry(new PageId(-1).pid, 0, Entry.HFPAGE_ENTRY);
		Entry initDir = new Entry(new PageId(-1).pid, 0, Entry.DIRECTORYPAGE_ENTRY);
		byte[] sEntry = SerialDir.getSerial(initHF);
		byte[] sDir = SerialDir.getSerial(initDir);

		System.arraycopy(sDir, 0, data, (count - 1) * size, size);
		for(int entryOffset = 0; entryOffset < count - 1; entryOffset++){
			System.arraycopy(sEntry, 0, data, entryOffset * size, size);			
		}
	}

	public int getTotalEntries(){
		return count;
	}

	public Entry getEntry(int offset) throws ChainException{
		Entry pDirEntry = null;
		byte[] entryData = new byte[size];
		System.arraycopy(data, offset * size, entryData, 0, size);
		pDirEntry = (Entry)SerialDir.getDeserial(entryData);
		return pDirEntry;
	}

	public Entry getNext() throws ChainException{
		return getEntry(count - 1);
	}

	public void setNext(Entry nextDirectoryEntry) throws ChainException{
		writeEntry(nextDirectoryEntry, count - 1);
	}

	public void writeEntry(Entry pDirEntry, int offset) throws ChainException{
		if(offset >= 0 && offset < count){
			byte[] entryData = SerialDir.getSerial(pDirEntry);
			System.arraycopy(entryData, 0, data, offset * size, entryData.length);
		}else{
			throw new ChainException(null, "Invalid directory offset: " + offset);
		}
	}

	private int getSize(Entry pDirEntry) throws ChainException{
		return SerialDir.getSerial(pDirEntry).length;
	}

	public boolean updateSpace(PageId hfPageId, int availableSpace) throws ChainException{
		int offset = 0;

		for(offset = 0; offset < count - 1; offset++){
			Entry pDirEntry = getEntry(offset);

			if(pDirEntry.getPID() == hfPageId.pid){
				//				pDirEntry.setAvailableSpace(availableSpace);
				writeEntry(pDirEntry, offset);
				return true;
			}
		}
		return false;
	}

	public PageId getSpace(int recordLength) throws ChainException{
		int offset = 0;

		for(offset = 0; offset < count - 1; offset++){
			Entry pDirEntry = getEntry(offset);

			if(pDirEntry.getPID() != -1){
				PageId hfPageId = new PageId(pDirEntry.getPID());
				Page foundPage = new Page();

				Minibase.BufferManager.pinPage(hfPageId, foundPage, false);
				if(HFPage.FREE_SPACE >= recordLength + HeapFilePage.SIZE_OF_SLOT){
					Minibase.BufferManager.unpinPage(hfPageId, false);
					return hfPageId;
				}else{
					Minibase.BufferManager.unpinPage(hfPageId, false);
				}
			}
		}
		for(offset = 0; offset < count - 1; offset++){
			Entry pDirEntry = getEntry(offset);

			if(pDirEntry.getPID() == -1){
				PageId newPageId = new PageId();
				Page newPage = new Page();

				newPageId = Minibase.BufferManager.newPage(newPage, 1);
				pDirEntry.setPID(newPageId.pid);
				writeEntry(pDirEntry, offset);
				Minibase.BufferManager.unpinPage(newPageId, true);

				return newPageId;

			}
		}
		return null;
	}
}


/* inner class for the Entry */
class Entry implements Serializable{
	/**
	 * 
	 */
	private static final long serialVersionUID = 1L;
	public static int HFPAGE_ENTRY = 1;
	public static int DIRECTORYPAGE_ENTRY = 2;

	private int pageId;
	private int type;

	public Entry(int pid, int availableSpace, int entryType){
		pageId = pid;
		type = entryType;
	}

	public Entry() {
		// TODO Auto-generated constructor stub
	}

	public int getPID(){
		return pageId;
	}

	public int getType(){
		return type;
	}

	public void setPID(int pid){
		pageId = pid;
	}
}

/**
 * Provides methods to serialize and deSerialize objects.
 * @author Maxwell Zhou
 *
 */
class SerialDir {
	/**
	 * Returns the serialized form of object.
	 * 
	 * @param object Object to serialize
	 * @return Serialized form of object. Null returned in case of an exception.
	 * @throws ChainException 
	 */
	public static byte[] getSerial(Object object) throws ChainException{
		byte[] serializedData = null;
		try{
			
			ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
			ObjectOutputStream objStream = new ObjectOutputStream(byteStream);
			objStream.writeObject(object);
			objStream.flush();
			byteStream.flush();
			serializedData = byteStream.toByteArray();
			objStream.close();
			byteStream.close();
		}catch(Exception e){
			throw new ChainException(e, null);
		}
		return serializedData;
	}
	
	/**
	 * Returns the deserialized form of data.
	 * 
	 * @param data Data to deserialize.
	 * @return Deserialized form of data.
	 * @throws ChainException 
	 */
	public static Object getDeserial(byte[] data) throws ChainException{
		Object object = null;
		
		try{
			ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(data);
			ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
			
			object = objectInputStream.readObject();
			
			// Close streams.
			objectInputStream.close();
			byteArrayInputStream.close();
		}catch(Exception e){
			throw new ChainException(e, null);
		}
		
		return object;
	}
}


